package com.googlecode.cswish.struts;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.servlet.ServletContext;

import org.apache.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.util.ClassLoaderUtils;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.FrameConstant;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.UnknownHandler;
import com.opensymphony.xwork2.XWorkException;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
import com.opensymphony.xwork2.config.entities.InterceptorMapping;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
import com.opensymphony.xwork2.config.providers.InterceptorBuilder;
import com.opensymphony.xwork2.inject.Inject;

/**
 * Create Action configuration and Result configuration
 * 
 * @author Jerry.Feng
 */
public class CSwishUnknownHandler implements UnknownHandler {

	private static final Logger logger = Logger
			.getLogger(CSwishUnknownHandler.class);

	protected boolean devMode;

	protected String defaultPackageName;
	
	protected Configuration configuration;

	@Inject
	protected ServletContext servletContext;

	protected ObjectFactory objectFactory;

	private Routing routing;
	
	private Config config;
	
	protected LinkedHashMap<String, ResultTypeConfig> resultsByExtension;
	
	@Inject
	public CSwishUnknownHandler(@Inject Configuration configuration, 
			@Inject("struts.cswish.defaultPackage") String defaultPackageName,
			@Inject(StrutsConstants.STRUTS_ACTION_EXTENSION) String actionExtension) {
		this.configuration = configuration;
		this.defaultPackageName = defaultPackageName;
		
        resultsByExtension = new LinkedHashMap<String,ResultTypeConfig>();
        PackageConfig parentPackage = configuration.getPackageConfig(defaultPackageName);
        if (parentPackage == null) {
            throw new ConfigurationException("Unknown parent package: " + parentPackage);
        }    
        Map<String,ResultTypeConfig> results = parentPackage.getAllResultTypeConfigs();
        
        // use FreeMarker as the default template 
        // (it will be override by the following correct template)
        resultsByExtension.put(actionExtension, results.get("freemarker"));
        // template list
        resultsByExtension.put("ftl", results.get("freemarker"));
        resultsByExtension.put("htm", results.get("freemarker"));
        resultsByExtension.put("jsp", results.get("dispatcher"));
        resultsByExtension.put("vm", results.get("velocity"));
        resultsByExtension.put("html", results.get("freemarker"));
	}

	
	@Inject(StrutsConstants.STRUTS_DEVMODE)
    public void setDevMode(String mode) {
        this.devMode = "true".equals(mode);
    }
	
	@Inject
	public void setObjectFactory(com.opensymphony.xwork2.ObjectFactory objectFactory) {
		this.objectFactory = (ObjectFactory) objectFactory;
		
		routing = this.objectFactory.getBean(Routing.class, false);
		config = this.objectFactory.getBean(Config.class, false);
	}

	@Override
	public ActionConfig handleUnknownAction(String namespace, String context)
			throws XWorkException {
		ActionConfig cfg = buildActionConfig(namespace, context, true);
		return cfg;
	}
	
	public ActionConfig buildActionConfig(String namespace, String context, boolean buildMatched) {
		PackageConfig packageConfig = configuration.getPackageConfig(defaultPackageName);

		Invoker invoker = routing.path2Invoker(context);
		
		// results
		Map<String, ResultConfig> defaultResults = packageConfig.getAllGlobalResults();
		Map<String, ResultConfig> results = new HashMap<String, ResultConfig>();
		results.putAll(defaultResults);
		
		// we need check the path dynamically in develop mode, so the result 
		// is created in 'handleUnknownResult'
		if (!devMode) {
			ResultConfig success = buildResult(Action.SUCCESS, context);
			results.put(Action.SUCCESS, success);
			// input page definition rule
			if (!invoker.isInputMethod()) {
				Invoker inputInvoker = new Invoker(invoker.actionName, invoker.separator, invoker.getInputMethod());
				String inputContext = routing.invoker2Path(inputInvoker);
				ResultConfig input = buildResult(Action.INPUT, inputContext);
				results.put(Action.INPUT, input);
			}
		}

		// parameter
		Map<String, String> params = new TreeMap<String, String>();

		// interceptor
		String defaultInterceptorRefName = packageConfig
				.getDefaultInterceptorRef();
		List<InterceptorMapping> interceptors = new ArrayList<InterceptorMapping>();
		if (defaultInterceptorRefName != null) {
			interceptors.addAll(InterceptorBuilder
					.constructInterceptorReference(packageConfig,
							defaultInterceptorRefName, new LinkedHashMap(),
							packageConfig.getLocation(), objectFactory));
		}

		// exception
		List<ExceptionMappingConfig> exceptionMappings = packageConfig.getAllExceptionMappingConfigs();
		
		// package name
		String packageName = defaultPackageName;
		
		// configuration
		String className = invoker.toString();
		objectFactory.registerAction(className);
		ActionConfig.Builder builder = new ActionConfig.Builder(packageName, null, className);
		builder.addResultConfigs(results);
		builder.addParams(params);
		builder.interceptors(interceptors);
		builder.exceptionMappings(exceptionMappings);
		ActionConfig config = builder.build();
		config.setLocation(packageConfig.getLocation());
		
		// FIXME: Out of Memory if there's no security filter
		Map<String, Map<String, ActionConfig>> actionConfigs = configuration.getRuntimeConfiguration().getActionConfigs();
		actionConfigs.get("").put(context, config);
		
		if (buildMatched) {
			String matchedMethod;
			if (invoker.isInputMethod()) {
				matchedMethod = invoker.getSubmitMethod();
			} else {
				matchedMethod = invoker.getInputMethod();
			}
			String newContext = routing.invoker2Path(new Invoker(invoker.actionName, invoker.separator, matchedMethod));
			if (!packageConfig.getActionConfigs().containsKey(newContext)) {
				buildActionConfig(namespace, newContext, false);
			}
		}
		
		return config;
	}

	private ResultConfig buildResult(String resultCode, String context) {
		HashMap<String, String> resultParams = new HashMap<String, String>();
		String location = findLocation(resultCode, context);
		String extension = location.substring(location.lastIndexOf('.') + 1);
		String resultClassName = resultsByExtension.get(extension).getClassName();
		if (logger.isDebugEnabled()) {
			logger.debug("Forward to Page: " + location);
		}
		resultParams.put("location", location);
		ResultConfig.Builder builder = new ResultConfig.Builder(resultCode, resultClassName);
		builder.addParams(resultParams);
		ResultConfig success = builder.build();
		return success;
	}

	private boolean checkPath(String path) {
		URL url = null;
		try {
			url = servletContext.getResource(path);
        	if (url == null) {
        		if (path.charAt(0) == '/') {
        			url = ClassLoaderUtils.getResource(path.substring(1), getClass());
        		} else {
        			url = ClassLoaderUtils.getResource(path, getClass());
        		}
            }
        } catch (Exception ex) {
        }
        return url != null ? true : false;
    }
	
	protected String findLocation(String resultCode, String context) {
		boolean found = false;
		String path = null;
		String suffix = null;

		// use the method name
		Invoker invoker = routing.path2Invoker(context);
		if (!resultCode.equals(Action.INPUT) && !resultCode.equals(Action.SUCCESS)) {
			invoker.methodName = resultCode;
		}
		
		
		// 1. search from the default folder
		String pathPrefix = config.getViewDir() + routing.invoker2Path(invoker) + '.';
		for (Iterator<String> iterator = resultsByExtension.keySet().iterator(); iterator.hasNext();) {
			suffix = iterator.next();
			path = pathPrefix + suffix;
			
			if (checkPath(path)) {
				found = true;
				break;
			}
		}
		
		// 2. skip the method and search again
		String prefix = null;
		if (!found) {
			int index = context.lastIndexOf(invoker.separator);
			if (index > 0) {
				prefix = context.substring(0, index);
			} else {
				prefix = context;
			}
			
			pathPrefix = config.getViewDir() + prefix + '.';
			for (Iterator<String> iterator = resultsByExtension.keySet().iterator(); iterator.hasNext();) {
				suffix = iterator.next();
				path = pathPrefix + suffix;
				
				if (checkPath(path)) {
					found = true;
					break;
				}
			}
		}
		
		// 3. search from the template folder
		//URL res = ClassLoaderUtils.getResource(path, this.getClass());		
		if (!found) {
			for (Iterator<String> iterator = resultsByExtension.keySet().iterator(); iterator.hasNext();) {
				suffix = iterator.next();
				// use the generic template
				path = config.getViewTemplate() 
					+ "generic." + invoker.methodName + '.' + suffix;
				
				if (checkPath(path)) {
					found = true;
					break;
				}
			}
		}
		
		// 3. give the developer a tip information
		if (!found) {
			if (invoker.methodName.startsWith(FrameConstant.ACTION_PREPARE)) {
				path = config.getViewTemplate() + "generic.input.ftl";
			} else {
				path = config.getViewTemplate() + "generic.$result.ftl";
			}
			
			if (devMode) {
				String location = formatLocation(config.getViewDir(),
						context, prefix, resultsByExtension.keySet());
				ActionContext.getContext().put(FrameConstant.STACK_LOCATION, location);
			}
		}
		return path;
	}
	
	private String formatLocation(String viewDir, String context, String prefix, Collection<String> extensions) {
		StringBuilder sb = new StringBuilder();
		
		for (String extension : extensions) {
			sb.append(viewDir).append(context).append('.').append(extension).append("<br>");
		}
		
		for (String extension : extensions) {
			sb.append(viewDir).append(prefix).append('.').append(extension).append("<br>");
		}
		return sb.toString();
	}
	
	@Override
	public Result handleUnknownResult(ActionContext actionContext,
			String actionName, ActionConfig actionConfig, String resultCode)
			throws XWorkException {
		String context = actionName;
		if (Action.INPUT.equals(resultCode)) {
			Invoker invoker = routing.path2Invoker(context);
			if (!invoker.isInputMethod()) {
				invoker.methodName = invoker.getInputMethod();
				context = routing.invoker2Path(invoker);
			}
		}
		ResultConfig resultConfig = buildResult(resultCode, context);
		try {
			return objectFactory.buildResult(resultConfig, actionContext.getContextMap());
		} catch (Exception e) {
            throw new XWorkException("Unable to build cwish result", e, resultConfig);
        }
	}

	@Override
	public Object handleUnknownActionMethod(Object action, String methodName)
			throws NoSuchMethodException {
		return null;
	}
}